/**
 * (created at 2011-1-27)
 */
package qiushuo.mysqlparser.recognizer.parser;

import static qiushuo.mysqlparser.recognizer.SQLTokenType.IDENTIFIER;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.KW_AS;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.KW_BY;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.KW_DESC;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.KW_FOR;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.KW_JOIN;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.KW_OUTER;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.PUNC_COMMA;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.PUNC_LEFT_PAREN;
import static qiushuo.mysqlparser.recognizer.SQLTokenType.PUNC_RIGHT_PAREN;

import java.sql.SQLSyntaxErrorException;
import java.util.LinkedList;
import java.util.List;

import qiushuo.mysqlparser.ast.expression.Expression;
import qiushuo.mysqlparser.ast.expression.primary.ParamMarker;
import qiushuo.mysqlparser.ast.fragment.Limit;
import qiushuo.mysqlparser.ast.fragment.OrderBy;
import qiushuo.mysqlparser.ast.fragment.tableref.IndexHint;
import qiushuo.mysqlparser.ast.fragment.tableref.InnerJoin;
import qiushuo.mysqlparser.ast.fragment.tableref.NaturalJoin;
import qiushuo.mysqlparser.ast.fragment.tableref.OuterJoin;
import qiushuo.mysqlparser.ast.fragment.tableref.StraightJoin;
import qiushuo.mysqlparser.ast.fragment.tableref.SubqueryFactor;
import qiushuo.mysqlparser.ast.fragment.tableref.TableRefFactor;
import qiushuo.mysqlparser.ast.fragment.tableref.TableReference;
import qiushuo.mysqlparser.ast.fragment.tableref.TableReferences;
import qiushuo.mysqlparser.ast.statement.DMLSelectUnionStatement;
import qiushuo.mysqlparser.recognizer.SQLToken;
import qiushuo.mysqlparser.recognizer.TokenReader;
import qiushuo.mysqlparser.util.Pair;

/**
 * @author <a href="mailto:QiuShuo1985@gmail.com">QIU Shuo</a>
 */
public abstract class DMLParser extends StatementParser {
    protected ExpressionParser exprParser;

    public DMLParser(TokenReader input) {
        super(input);
        this.exprParser = new ExpressionParser(input);
    }

    /**
     * no non-belonging token is consumed
     * 
     * @throws SQLSyntaxErrorException
     */
    protected TableReferences tableRefs() throws SQLSyntaxErrorException {
        List<TableReference> list = new LinkedList<TableReference>();
        while (true) {
            TableReference tr = tableReference();
            list.add(tr);
            if (input.isNext(PUNC_COMMA, 0)) {
                input.skipForce(1);
            } else {
                return new TableReferences(list);
            }
        }
    }

    private TableReference tableReference() throws SQLSyntaxErrorException {
        TableReference tr = tableFactor();
        loop1: while (true) {
            Expression on;
            List<String> using;
            TableReference trNew;
            boolean isOut = false;
            boolean isLeft = true;
            SQLToken token = input.next();
            if (token == null) break;
            switch (token.getType()) {
            case KW_INNER:
            case KW_CROSS:
                match(KW_JOIN);
            case KW_JOIN:
                trNew = tableFactor();
                token = input.next();
                if (token == null) return new InnerJoin(tr, trNew);
                switch (token.getType()) {
                case KW_ON:
                    on = exprParser.expression();
                    tr = new InnerJoin(tr, trNew, on);
                    break;
                case KW_USING:
                    using = using();
                    tr = new InnerJoin(tr, trNew, using);
                    break;
                default:
                    input.backForce();
                    tr = new InnerJoin(tr, trNew);
                    break;
                }
                break;
            case KW_STRAIGHT_JOIN:
                trNew = tableFactor();
                token = input.next();
                if (token == null) return new StraightJoin(tr, trNew);
                switch (token.getType()) {
                case KW_ON:
                    on = exprParser.expression();
                    tr = new StraightJoin(tr, trNew, on);
                    break;
                default:
                    input.backForce();
                    tr = new StraightJoin(tr, trNew);
                    break;
                }
                break;
            case KW_RIGHT:
                isLeft = false;
            case KW_LEFT:
                if (input.isNext(KW_OUTER, 0)) {
                    input.skipForce(1);
                }
                match(KW_JOIN);
                trNew = tableReference();
                token = input.next();
                if (token == null) throw err("conditionExpr cannot be null for outer join");
                switch (token.getType()) {
                case KW_ON:
                    on = exprParser.expression();
                    tr = new OuterJoin(isLeft, tr, trNew, on);
                    break;
                case KW_USING:
                    using = using();
                    tr = new OuterJoin(isLeft, tr, trNew, using);
                    break;
                default:
                    throw err("expected token for conditionExpr of outer join: " + token);
                }
                break;
            case KW_NATURAL:
                token = input.next();
                if (token == null) throw err("EOF after NATURAL for natural join");
                switch (token.getType()) {
                case KW_RIGHT:
                    isLeft = false;
                case KW_LEFT:
                    if (input.isNext(KW_OUTER, 0)) {
                        input.skipForce(1);
                    }
                    isOut = true;
                    match(KW_JOIN);
                case KW_JOIN:
                    trNew = tableFactor();
                    tr = new NaturalJoin(isOut, isLeft, tr, trNew);
                    break;
                default:
                    throw err("unexpected token after NATURAL for natural join:" + token);
                }
                break;
            default:
                input.backForce();
                break loop1;
            }
        }
        return tr;
    }

    /** first <code>USING</code> has been consumed */
    private List<String> using() throws SQLSyntaxErrorException {
        List<String> list = new LinkedList<String>();
        match(PUNC_LEFT_PAREN);
        while (true) {
            SQLToken col = match(IDENTIFIER);
            list.add(col.getText());
            SQLToken token = input.next();
            if (token == null) throw err("broken columnList for USING condExpr: EOF");
            switch (token.getType()) {
            case PUNC_COMMA:
                continue;
            case PUNC_RIGHT_PAREN:
                return list;
            default:
                throw err("broken columnList for USING condExpr: " + token);
            }
        }
    }

    private TableReference tableFactor() throws SQLSyntaxErrorException {
        SQLToken token = input.next();
        if (token == null) throw err("EOF for tableFactor");
        String alias = null;
        switch (token.getType()) {
        case IDENTIFIER:
            String tableName = token.getText();
            token = input.next();
            if (token == null) {
                return new TableRefFactor(tableName, null);
            }
            switch (token.getType()) {
            case KW_AS:
                token = match(IDENTIFIER);
            case IDENTIFIER:
                alias = token.getText();
                break;
            case KW_USE:
            case KW_IGNORE:
            case KW_FORCE:
                input.backForce();
                break;
            default:
                input.backForce();
                return new TableRefFactor(tableName, null);
            }
            List<IndexHint> hint = hint();
            return new TableRefFactor(tableName, alias, hint);
        case PUNC_LEFT_PAREN:
            int mark = input.getCurrentIndex();
            try {
                DMLSelectUnionStatement subQuery = new DMLSelectStmtParser(input).selectUnionStmt();
                if (input.isNext(KW_AS, 0)) {
                    input.skipForce(1);
                }
                token = match(IDENTIFIER);
                alias = token.getText();
                return new SubqueryFactor(subQuery, alias);
            } catch (SQLSyntaxErrorException sse) {
                input.skipTo(mark);
                return tableRefs();
            } finally {
                match(PUNC_RIGHT_PAREN);
            }
        default:
            throw err("unexpected token for tableFactor: " + token);
        }
    }

    /**
     * @return empty if there is no hint
     */
    private List<IndexHint> hint() throws SQLSyntaxErrorException {
        List<IndexHint> list = new LinkedList<IndexHint>();
        while (true) {
            SQLToken token = input.next();
            if (token == null) return list;
            IndexHint.IndexAction action = null;
            switch (token.getType()) {
            case KW_USE:
                action = IndexHint.IndexAction.USE;
                break;
            case KW_IGNORE:
                action = IndexHint.IndexAction.IGNORE;
                break;
            case KW_FORCE:
                action = IndexHint.IndexAction.FORCE;
                break;
            default:
                input.backForce();
                return list;
            }
            // 1 token skipped
            IndexHint.IndexType type = null;
            token = input.next();
            if (token == null) {
                input.backForce();
                return list;
            }
            switch (token.getType()) {
            case KW_INDEX:
                type = IndexHint.IndexType.INDEX;
                break;
            case KW_KEY:
                type = IndexHint.IndexType.KEY;
                break;
            default:
                input.skipForce(-2);
                return list;
            }
            // 2 token skipped
            IndexHint.IndexScope scope = IndexHint.IndexScope.ALL;
            if (input.isNext(KW_FOR, 0)) {
                input.skipForce(1);
                token = input.next();
                if (token == null) {
                    input.skipForce(-3);
                    return list;
                }
                switch (token.getType()) {
                case KW_JOIN:
                    scope = IndexHint.IndexScope.JOIN;
                    break;
                case KW_ORDER:
                    if (input.isNext(KW_BY, 0)) {
                        input.skipForce(1);
                        scope = IndexHint.IndexScope.ORDER_BY;
                        break;
                    }
                case KW_GROUP:
                    if (input.isNext(KW_BY, 0)) {
                        input.skipForce(1);
                        scope = IndexHint.IndexScope.GROUP_BY;
                        break;
                    }
                default:
                    input.skipForce(-4);
                    return list;
                }
            }
            match(PUNC_LEFT_PAREN);
            if (IndexHint.IndexAction.USE.equals(action) && input.isNext(PUNC_RIGHT_PAREN, 0)) {
                input.skipForce(1);
                list.add(new IndexHint(action, type, scope, null));
                continue;
            }
            List<String> indexList = new LinkedList<String>();
            loopIndexList: while (true) {
                token = match(IDENTIFIER);
                indexList.add(token.getText());
                token = input.next();
                if (token == null) throw err("EOF for index list");
                switch (token.getType()) {
                case PUNC_COMMA:
                    continue;
                case PUNC_RIGHT_PAREN:
                    list.add(new IndexHint(action, type, scope, indexList));
                    break loopIndexList;
                default:
                    throw err("unexpected token for index list: " + token);
                }
            }
        }
    }

    /**
     * <code>ORDER BY</code> have been consumed. ORDER BY NULL is permitted
     */
    protected OrderBy orderBy() throws SQLSyntaxErrorException {
        List<Pair<Expression, Boolean>> eles = new LinkedList<Pair<Expression, Boolean>>();
        while (true) {
            Expression expr = exprParser.expression();
            SQLToken token = input.next();
            if (token == null) {
                eles.add(new Pair<Expression, Boolean>(expr, false));
                return new OrderBy(eles);
            }
            switch (token.getType()) {
            case PUNC_COMMA:
                eles.add(new Pair<Expression, Boolean>(expr, false));
                continue;
            case KW_ASC:
            case KW_DESC:
                eles.add(new Pair<Expression, Boolean>(expr, KW_DESC.equals(token.getType())));
                token = input.next();
                if (token == null) {
                    return new OrderBy(eles);
                }
                switch (token.getType()) {
                case PUNC_COMMA:
                    continue;
                }
            default:
                input.backForce();
                return new OrderBy(eles);
            }
        }
    }

    /**
     * <code>LIMIT</code> have been consumed
     */
    protected Limit limit() throws SQLSyntaxErrorException {
        SQLToken token = input.next();
        if (token == null) throw err("EOF for limit");
        String num1 = null;
        switch (token.getType()) {
        case SC_QUESTION:
            break;
        case LITERAL_NUMBER:
            num1 = token.getText();
            break;
        default:
            throw err("unrecognized token for limit: " + token);
        }
        token = input.next();
        if (token == null) {
            return (num1 == null) ? new Limit("0", new ParamMarker()) : new Limit("0", num1);
        }
        switch (token.getType()) {
        case PUNC_COMMA:
            token = input.next();
            if (token == null) throw err("EOF for limit");
            switch (token.getType()) {
            case SC_QUESTION:
                return (num1 == null) ? new Limit(new ParamMarker(), new ParamMarker()) : new Limit(
                        num1,
                        new ParamMarker());
            case LITERAL_NUMBER:
                return (num1 == null)
                        ? new Limit(new ParamMarker(), token.getText())
                        : new Limit(num1, token.getText());
            default:
                throw err("unrecognized token for limit: " + token);
            }
        case IDENTIFIER:
            if ("OFFSET".equals(token.getText().toUpperCase())) {
                token = input.next();
                if (token == null) throw err("EOF for limit");
                switch (token.getType()) {
                case SC_QUESTION:
                    return (num1 == null) ? new Limit(new ParamMarker(), new ParamMarker()) : new Limit(
                            new ParamMarker(),
                            num1);
                case LITERAL_NUMBER:
                    return (num1 == null) ? new Limit(token.getText(), new ParamMarker()) : new Limit(
                            token.getText(),
                            num1);
                default:
                    throw err("unrecognized token for limit: " + token);
                }
            }
        default:
            input.backForce();
            return (num1 == null) ? new Limit("0", new ParamMarker()) : new Limit("0", num1);
        }
    }
}
